﻿// ***********************************************************************
// Provided for Informational Purposes Only
//
// Apache 2.0 License
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
//
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY 
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR 
// PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
//
// See the Apache Version 2.0 License for specific language governing
// permissions and limitations under the License.
// ***********************************************************************
// Assembly  : ContractMeow.ModernApp
// File      : SuspensionManager.cs
//
// ***********************************************************************
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace ContractMeow.ModernApp.Common
{
	/// <summary>
	/// SuspensionManager captures global session state to simplify process lifetime management
	/// for an application.  Note that session state will be automatically cleared under a variety
	/// of conditions and should only be used to store information that would be convenient to
	/// carry across sessions, but that should be discarded when an application crashes or is
	/// upgraded.
	/// </summary>
	internal sealed class SuspensionManager
	{
		private static Dictionary<string, object> _sessionState = new Dictionary<string, object>();
		private static List<Type> _knownTypes = new List<Type>();
		private const string sessionStateFilename = "_sessionState.xml";

		/// <summary>
		/// Provides access to global session state for the current session.  This state is
		/// serialized by <see cref="SaveAsync"/> and restored by
		/// <see cref="RestoreAsync"/>, so values must be serializable by
		/// <see cref="DataContractSerializer"/> and should be as compact as possible.  Strings
		/// and other self-contained data types are strongly recommended.
		/// </summary>
		public static Dictionary<string, object> SessionState
		{
			get { return _sessionState; }
		}

		/// <summary>
		/// List of custom types provided to the <see cref="DataContractSerializer"/> when
		/// reading and writing session state.  Initially empty, additional types may be
		/// added to customize the serialization process.
		/// </summary>
		public static List<Type> KnownTypes
		{
			get { return _knownTypes; }
		}

		/// <summary>
		/// Save the current <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
		/// registered with <see cref="RegisterFrame"/> will also preserve their current
		/// navigation stack, which in turn gives their active <see cref="Page"/> an opportunity
		/// to save its state.
		/// </summary>
		/// <returns>An asynchronous task that reflects when session state has been saved.</returns>
		public static async Task SaveAsync()
		{
			try
			{
				// Save the navigation state for all registered frames
				foreach (var weakFrameReference in _registeredFrames)
				{
					Frame frame;
					if (weakFrameReference.TryGetTarget(out frame))
					{
						SaveFrameNavigationState(frame);
					}
				}

				// Serialize the session state synchronously to avoid asynchronous access to shared
				// state
				MemoryStream sessionData = new MemoryStream();
				DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
				serializer.WriteObject(sessionData, _sessionState);

				// Get an output stream for the SessionState file and write the state asynchronously
				StorageFile file = await ApplicationData.Current.LocalFolder.CreateFileAsync(sessionStateFilename, CreationCollisionOption.ReplaceExisting);
				using (Stream fileStream = await file.OpenStreamForWriteAsync())
				{
					sessionData.Seek(0, SeekOrigin.Begin);
					await sessionData.CopyToAsync(fileStream);
					await fileStream.FlushAsync();
				}
			}
			catch (Exception e)
			{
				throw new SuspensionManagerException(e);
			}
		}

		/// <summary>
		/// Restores previously saved <see cref="SessionState"/>.  Any <see cref="Frame"/> instances
		/// registered with <see cref="RegisterFrame"/> will also restore their prior navigation
		/// state, which in turn gives their active <see cref="Page"/> an opportunity restore its
		/// state.
		/// </summary>
		/// <returns>An asynchronous task that reflects when session state has been read.  The
		/// content of <see cref="SessionState"/> should not be relied upon until this task
		/// completes.</returns>
		public static async Task RestoreAsync()
		{
			_sessionState = new Dictionary<String, Object>();

			try
			{
				// Get the input stream for the SessionState file
				StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync(sessionStateFilename);
				using (IInputStream inStream = await file.OpenSequentialReadAsync())
				{
					// Deserialize the Session State
					DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
					_sessionState = (Dictionary<string, object>)serializer.ReadObject(inStream.AsStreamForRead());
				}

				// Restore any registered frames to their saved state
				foreach (var weakFrameReference in _registeredFrames)
				{
					Frame frame;
					if (weakFrameReference.TryGetTarget(out frame))
					{
						frame.ClearValue(FrameSessionStateProperty);
						RestoreFrameNavigationState(frame);
					}
				}
			}
			catch (Exception e)
			{
				throw new SuspensionManagerException(e);
			}
		}

		private static DependencyProperty FrameSessionStateKeyProperty =
			DependencyProperty.RegisterAttached("_FrameSessionStateKey", typeof(String), typeof(SuspensionManager), null);
		private static DependencyProperty FrameSessionStateProperty =
			DependencyProperty.RegisterAttached("_FrameSessionState", typeof(Dictionary<String, Object>), typeof(SuspensionManager), null);
		private static List<WeakReference<Frame>> _registeredFrames = new List<WeakReference<Frame>>();

		/// <summary>
		/// Registers a <see cref="Frame"/> instance to allow its navigation history to be saved to
		/// and restored from <see cref="SessionState"/>.  Frames should be registered once
		/// immediately after creation if they will participate in session state management.  Upon
		/// registration if state has already been restored for the specified key
		/// the navigation history will immediately be restored.  Subsequent invocations of
		/// <see cref="RestoreAsync"/> will also restore navigation history.
		/// </summary>
		/// <param name="frame">An instance whose navigation history should be managed by
		/// <see cref="SuspensionManager"/></param>
		/// <param name="sessionStateKey">A unique key into <see cref="SessionState"/> used to
		/// store navigation-related information.</param>
		public static void RegisterFrame(Frame frame, String sessionStateKey)
		{
			if (frame.GetValue(FrameSessionStateKeyProperty) != null)
			{
				throw new InvalidOperationException("Frames can only be registered to one session state key");
			}

			if (frame.GetValue(FrameSessionStateProperty) != null)
			{
				throw new InvalidOperationException("Frames must be either be registered before accessing frame session state, or not registered at all");
			}

			// Use a dependency property to associate the session key with a frame, and keep a list of frames whose
			// navigation state should be managed
			frame.SetValue(FrameSessionStateKeyProperty, sessionStateKey);
			_registeredFrames.Add(new WeakReference<Frame>(frame));

			// Check to see if navigation state can be restored
			RestoreFrameNavigationState(frame);
		}

		/// <summary>
		/// Disassociates a <see cref="Frame"/> previously registered by <see cref="RegisterFrame"/>
		/// from <see cref="SessionState"/>.  Any navigation state previously captured will be
		/// removed.
		/// </summary>
		/// <param name="frame">An instance whose navigation history should no longer be
		/// managed.</param>
		public static void UnregisterFrame(Frame frame)
		{
			// Remove session state and remove the frame from the list of frames whose navigation
			// state will be saved (along with any weak references that are no longer reachable)
			SessionState.Remove((String)frame.GetValue(FrameSessionStateKeyProperty));
			_registeredFrames.RemoveAll((weakFrameReference) =>
			{
				Frame testFrame;
				return !weakFrameReference.TryGetTarget(out testFrame) || testFrame == frame;
			});
		}

		/// <summary>
		/// Provides storage for session state associated with the specified <see cref="Frame"/>.
		/// Frames that have been previously registered with <see cref="RegisterFrame"/> have
		/// their session state saved and restored automatically as a part of the global
		/// <see cref="SessionState"/>.  Frames that are not registered have transient state
		/// that can still be useful when restoring pages that have been discarded from the
		/// navigation cache.
		/// </summary>
		/// <remarks>Apps may choose to rely on <see cref="LayoutAwarePage"/> to manage
		/// page-specific state instead of working with frame session state directly.</remarks>
		/// <param name="frame">The instance for which session state is desired.</param>
		/// <returns>A collection of state subject to the same serialization mechanism as
		/// <see cref="SessionState"/>.</returns>
		public static Dictionary<String, Object> SessionStateForFrame(Frame frame)
		{
			var frameState = (Dictionary<String, Object>)frame.GetValue(FrameSessionStateProperty);

			if (frameState == null)
			{
				var frameSessionKey = (String)frame.GetValue(FrameSessionStateKeyProperty);
				if (frameSessionKey != null)
				{
					// Registered frames reflect the corresponding session state
					if (!_sessionState.ContainsKey(frameSessionKey))
					{
						_sessionState[frameSessionKey] = new Dictionary<String, Object>();
					}
					frameState = (Dictionary<String, Object>)_sessionState[frameSessionKey];
				}
				else
				{
					// Frames that aren't registered have transient state
					frameState = new Dictionary<String, Object>();
				}
				frame.SetValue(FrameSessionStateProperty, frameState);
			}
			return frameState;
		}

		private static void RestoreFrameNavigationState(Frame frame)
		{
			var frameState = SessionStateForFrame(frame);
			if (frameState.ContainsKey("Navigation"))
			{
				frame.SetNavigationState((String)frameState["Navigation"]);
			}
		}

		private static void SaveFrameNavigationState(Frame frame)
		{
			var frameState = SessionStateForFrame(frame);
			frameState["Navigation"] = frame.GetNavigationState();
		}
	}
	public class SuspensionManagerException : Exception
	{
		public SuspensionManagerException()
		{
		}

		public SuspensionManagerException(Exception e)
			: base("SuspensionManager failed", e)
		{

		}
	}
}
